/*
 *  Copyright (c) 2011 Leibniz Institute of Plant Genetics and Crop Plant Research (IPK), Gatersleben, Germany.
 *  All rights reserved. This program and the accompanying materials
 *  are made available under the terms of the GNU Lesser Public License v2.1
 *  which accompanies this distribution, and is available at
 *  http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
 *
 *  Contributors:
 *      Leibniz Institute of Plant Genetics and Crop Plant Research (IPK), Gatersleben, Germany - initial API and implementation
 */
package de.ipk_gatersleben.bit.bi.edal.sample;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Properties;
import javax.security.auth.Subject;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.kerberos.KerberosPrincipal;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
import javax.swing.JOptionPane;

import org.apache.commons.lang.SystemUtils;
import org.hibernate.stat.SecondLevelCacheStatistics;
import org.hibernate.stat.Statistics;

import de.ipk_gatersleben.bit.bi.edal.primary_data.EdalConfiguration;
import de.ipk_gatersleben.bit.bi.edal.primary_data.file.EdalException;
import de.ipk_gatersleben.bit.bi.edal.primary_data.file.ImplementationProvider;
import de.ipk_gatersleben.bit.bi.edal.primary_data.file.implementation.FileSystemImplementationProvider;
import de.ipk_gatersleben.bit.bi.edal.primary_data.security.EdalAuthenticateException;
import de.ipk_gatersleben.bit.bi.edal.sample.login.LoginCallbackHandler;
import de.ipk_gatersleben.bit.bi.edal.sample.login.NewCallBackHandler;
import de.ipk_gatersleben.bit.bi.edal.sample.login.OAuthCallBackHandler;

/**
 * Provide some helpful function for the first experience with eDAL.
 * 
 * @author arendd
 */
public class EdalHelpers {

	/**
	 * Authenticate user using the IPK Kerberos-LoginModule.
	 * 
	 * @return the authenticated {@link Subject}
	 * @throws EdalAuthenticateException
	 *             if unable to run {@link javax.security.auth.spi.LoginModule}
	 *             successful.
	 */
	public static Subject authenticateIPKKerberosUser()
			throws EdalAuthenticateException {
		return EdalHelpers.authenticateSubjectWithKerberos(
				"KerberosServer.properties", null);
	}

	/**
	 * Authenticate user using the IPK Kerberos-LoginModule.
	 * 
	 * @param user
	 *            user name.
	 * 
	 * @return the authenticated {@link Subject}
	 * @throws EdalAuthenticateException
	 *             if unable to run {@link javax.security.auth.spi.LoginModule}
	 *             successful.
	 */
	public static Subject authenticateIPKKerberosUser(String user)
			throws EdalAuthenticateException {
		return EdalHelpers.authenticateSubjectWithKerberos(
				"KerberosServer.properties", user);
	}

	/**
	 * Authenticate user using the
	 * {@link de.ipk_gatersleben.bit.bi.edal.sample.login.SampleLoginModule}
	 * -LoginModule.
	 * 
	 * @return the authenticated {@link Subject}
	 * @throws EdalAuthenticateException
	 *             if unable to run {@link javax.security.auth.spi.LoginModule}
	 *             successful.
	 */
	public static Subject authenticateSampleUser()
			throws EdalAuthenticateException {

		return EdalHelpers.authenticateSubject("LoginModules.properties",
				"Sample", null);
	}

	/**
	 * Authenticate user using the
	 * {@link de.ipk_gatersleben.bit.bi.edal.sample.login.NewSampleLoginModule}
	 * -LoginModule.
	 * 
	 * @return the authenticated {@link Subject}
	 * @throws EdalAuthenticateException
	 *             if unable to run {@link javax.security.auth.spi.LoginModule}
	 *             successful.
	 */
	public static Subject authenticateNewSampleUser(String name, String password)
			throws EdalAuthenticateException {

		return EdalHelpers.authenticateSubject("LoginModules.properties",
				"NewSample", new NewCallBackHandler(name, password));
	}
	
	
	
	public static Subject authenticateOAuthUser(String name, String password)
			throws EdalAuthenticateException {

		return EdalHelpers.authenticateSubject("LoginModules.properties",
				"OAuth", new OAuthCallBackHandler(name, password));
	}

	/**
	 * Authenticate user using specified loginmodule.properties.
	 * 
	 * @param loginModulePropertyFile
	 *            the {@link java.io.File} that consists all
	 *            {@link javax.security.auth.spi.LoginModule}
	 * 
	 *            s.
	 * @param loginModule
	 *            the choose {@link javax.security.auth.spi.LoginModule}.
	 * @return the authenticated {@link Subject}
	 * @throws EdalAuthenticateException
	 *             if unable to run {@link javax.security.auth.spi.LoginModule}
	 *             successful.
	 */
	public static Subject authenticateSubject(
			final String loginModulePropertyFile, final String loginModule)
			throws EdalAuthenticateException {
		return EdalHelpers.authenticateSubject(loginModulePropertyFile,
				loginModule, null);
	}

	/**
	 * Authenticate user using specified loginmodule.properties an
	 * {@link CallbackHandler}.
	 * 
	 * @param loginModulePropertyFile
	 *            the {@link java.io.File} that consists all
	 *            {@link javax.security.auth.spi.LoginModule}s.
	 * @param loginModule
	 *            the choose {@link javax.security.auth.spi.LoginModule}.
	 * @param callbackhandler
	 *            a {@link CallbackHandler} for the specified
	 *            {@link javax.security.auth.spi.LoginModule}
	 * @return the authenticated {@link Subject}
	 * @throws EdalAuthenticateException
	 *             if unable to run {@link javax.security.auth.spi.LoginModule}
	 *             successful.
	 */
	public static Subject authenticateSubject(
			final String loginModulePropertyFile, final String loginModule,
			final CallbackHandler callbackhandler)
			throws EdalAuthenticateException {
		try {
			final String config = EdalHelpers.class.getResource(
					loginModulePropertyFile).toString();
			System.setProperty("java.security.auth.login.config", config);

		} catch (final Exception e) {
			throw new EdalAuthenticateException(
					"can't load LoginModule property file", e);
		}

		LoginContext ctx = null;
		try {
			if (callbackhandler == null) {
				ctx = new LoginContext(loginModule);
			} else {
				ctx = new LoginContext(loginModule, callbackhandler);
			}
			ctx.login();
		} catch (final LoginException e) {
			throw new EdalAuthenticateException(
					"can't login using LoginModule", e);
		}
		return ctx.getSubject();

	}

	/**
	 * Authenticate user using the specified Kerberos-LoginModule.
	 * 
	 * @param kerberosPropertyFile
	 *            the {@link java.io.File} that defines all necessary kerberos
	 *            properties.
	 * @param user
	 *            optional username
	 * @return the authenticated {@link Subject}
	 * @throws EdalAuthenticateException
	 *             if unable to run {@link javax.security.auth.spi.LoginModule}
	 *             successful.
	 */
	@SuppressWarnings("restriction")
	public static Subject authenticateSubjectWithKerberos(
			final String kerberosPropertyFile, String user)
			throws EdalAuthenticateException {

		try {
			final String loginConfig = EdalHelpers.class.getResource(
					"LoginModules.properties").toString();

			final Properties properties = new Properties();
			try (InputStream is = EdalHelpers.class
					.getResourceAsStream(kerberosPropertyFile)) {

				properties.load(is);
			} catch (final IOException e) {
				throw new EdalAuthenticateException(
						"unable to load Kerberos server properties", e);
			}
			if (!properties.containsKey("realm")) {
				throw new EdalAuthenticateException("no 'realm' paramter set");
			}
			if (!properties.containsKey("kdc")) {
				throw new EdalAuthenticateException("no 'kdc' paramter set");
			}

			System.setProperty("java.security.auth.login.config", loginConfig);

			System.setProperty("java.security.krb5.realm",
					properties.getProperty("realm"));
			System.setProperty("java.security.krb5.kdc",
					properties.getProperty("kdc"));

		} catch (final Exception e) {
			throw new EdalAuthenticateException(
					"can't load LoginModule property file ", e);
		}
		LoginContext ctx = null;

		boolean retry = true;

		while (retry) {
			try {
				ctx = new LoginContext("Kerberos", new LoginCallbackHandler(
						user));
				ctx.login();
				retry = false;
				return ctx.getSubject();
			} catch (final LoginException e) {

				if (e.getCause() == null) {
					return null;
				}

				else if (e.getCause().getClass()
						.equals(sun.security.krb5.KrbException.class)) {
					int result = (Integer) JOptionPane
							.showConfirmDialog(
									null,
									"Your login attempt was not successful, try again. \nReason: Bad credentials.",
									"Login to IPK-Domain (LDAP)",
									JOptionPane.YES_NO_OPTION);

					if (result == JOptionPane.YES_OPTION) {
						retry = true;
					} else if (result == JOptionPane.NO_OPTION) {
						return null;
					}
				}
			}
		}
		return null;

	}

	/**
	 * Authenticate user using the Windows- or Unix-LoginModule.
	 * 
	 * @return authenticated {@link Subject}
	 * @throws EdalAuthenticateException
	 */
	public static Subject authenticateWinOrUnixUser()
			throws EdalAuthenticateException {

		if (SystemUtils.IS_OS_UNIX) {
			return EdalHelpers.authenticateSubject("LoginModules.properties",
					"Unix", null);
		}

		else if (SystemUtils.IS_OS_WINDOWS) {
			return EdalHelpers.authenticateSubject("LoginModules.properties",
					"Windows", null);
		} else {
			throw new EdalAuthenticateException(
					"You not using a Windows or Unix OS !");
		}

	}

	/**
	 * Clean all files in directory.
	 * 
	 * @param path
	 *            a {@link Path} object.
	 * @throws EdalException
	 *             if unable to clean mount path.
	 */
	public static void cleanMountPath(final Path path) throws EdalException {
		try {
			EdalHelpers.deleteDir(path);
		} catch (EdalException e) {
			throw new EdalException("Can not clean mount path: "
					+ e.getMessage());
		}
	}

	/**
	 * Delete all files in a directory recursively.
	 * 
	 * @param directory
	 *            to delete
	 * @throws EdalException
	 *             if unable to clean directory
	 */
	private static void deleteDir(final Path directory) throws EdalException {

		if (Files.exists(directory, LinkOption.NOFOLLOW_LINKS)) {
			try {
				Files.walkFileTree(directory, new SimpleFileVisitor<Path>() {

					@Override
					public FileVisitResult visitFile(Path file,
							BasicFileAttributes attrs) throws IOException {
						while (file.toFile().delete()) {
							synchronized (this) {
								try {
									this.wait(10);
								} catch (InterruptedException e) {
									throw new IOException(
											"could not wait to delete file: "
													+ e.getMessage(), e);
								}
							}
						}
						return FileVisitResult.CONTINUE;
					}

					@Override
					public FileVisitResult postVisitDirectory(Path dir,
							IOException exception) throws IOException {

						if (exception == null) {

							while (dir.toFile().delete()) {
								synchronized (this) {
									try {
										this.wait(10);
									} catch (InterruptedException e) {
										throw new IOException(
												"could not wait to delete file: "
														+ e.getMessage(), e);
									}
								}
							}
							return FileVisitResult.CONTINUE;
						} else {
							throw exception;
						}
					}

				});
			} catch (IOException e) {
				throw new EdalException("could not delete file: "
						+ e.getMessage());
			}
		}
	}

	/**
	 * Get a new {@link FileSystemImplementationProvider}. The path to use for
	 * file storage and database is set to users home/edal_test
	 * 
	 * @param cleanMountPathBefore
	 *            true if you want to clean the database before start eDAL.
	 * @param config
	 *            the {@link EdalConfiguration} class.
	 * @return new {@link FileSystemImplementationProvider}
	 * @throws EdalException
	 *             if unable to clean mount path or create new mount path.
	 */
	public static ImplementationProvider getFileSystemImplementationProvider(
			final boolean cleanMountPathBefore, EdalConfiguration config)
			throws EdalException {

		if (cleanMountPathBefore) {
			try {
				EdalHelpers.cleanMountPath(config.getMountPath());
				EdalHelpers.cleanMountPath(config.getDataPath());

			} catch (EdalException e) {
				throw new EdalException(
						"Can not clean mount path before starting eDAL: "
								+ e.getMessage(), e);
			}
		}

		if (Files.notExists(config.getMountPath(), LinkOption.NOFOLLOW_LINKS)) {
			try {
				Files.createDirectories(config.getMountPath());
			} catch (IOException e) {
				throw new EdalException(
						"Can not create mount path before starting eDAL: "
								+ e.getMessage(), e);
			}
		}
		if (Files.notExists(config.getDataPath(), LinkOption.NOFOLLOW_LINKS)) {
			try {
				Files.createDirectories(config.getDataPath());
			} catch (IOException e) {
				throw new EdalException(
						"Can not create data path before start eDAL: "
								+ e.getMessage(), e);
			}
		}

		final ImplementationProvider mountPoint = new FileSystemImplementationProvider(
				config);

		return mountPoint;
	}

	/**
	 * Print PermissionStatistic.
	 * 
	 * @param statistics
	 */
	public static void getStatistic(final Statistics statistics) {

		try {
			if (!statistics.isStatisticsEnabled()) {
				System.out.println("WARN: statistics disabled");
			}
			System.out.println("\n******** Permission-Statistic ********");

			SecondLevelCacheStatistics rootStatistics = null;

			try {
				rootStatistics = statistics
						.getSecondLevelCacheStatistics("query.root");
				final long rootHit = rootStatistics.getHitCount();

				final long rootMiss = rootStatistics.getMissCount();

				final double rootRatio = (double) rootHit
						/ (double) (rootHit + rootMiss);

				System.out.println("\nquery.root-Cache:");
				System.out.println("Hit-Ratio : " + rootRatio);
				System.out.println("Hits : " + rootHit);
				System.out.println("Miss : " + rootMiss);

			} catch (final NullPointerException e) {
				System.out.println("didnt use query.root-Cache");
			}

			SecondLevelCacheStatistics permissionStatistics = null;

			try {
				permissionStatistics = statistics
						.getSecondLevelCacheStatistics("query.permission");
				final long permissionHit = permissionStatistics.getHitCount();

				final long permissionMiss = permissionStatistics.getMissCount();

				final double permissionRatio = (double) permissionHit
						/ (double) (permissionHit + permissionMiss);

				System.out.println("\nquery.permission-Cache:");
				System.out.println("Hit-Ratio : " + permissionRatio);
				System.out.println("Hits : " + permissionHit);
				System.out.println("Miss : " + permissionMiss);

			} catch (final NullPointerException e) {
				System.out.println("didnt use query.permission-Cache");
			}

			SecondLevelCacheStatistics principalStatistics = null;

			try {
				principalStatistics = statistics
						.getSecondLevelCacheStatistics("query.principal");
				final long principalHit = principalStatistics.getHitCount();
				final long principalMiss = principalStatistics.getMissCount();

				final double principalRatio = (double) principalHit
						/ (double) (principalHit + principalMiss);

				System.out.println("\nquery.principal-Cache:");
				System.out.println("Hit-Ratio : " + principalRatio);
				System.out.println("Hits : " + principalHit);
				System.out.println("Miss : " + principalMiss);
				System.out.println("\n**************************************");

			} catch (final NullPointerException e) {
				System.out.println("didnt use query.principal-Cache");
			}

			SecondLevelCacheStatistics listStatistics = null;

			try {
				listStatistics = statistics
						.getSecondLevelCacheStatistics("query.list");
				final long listHit = listStatistics.getHitCount();

				final long listMiss = listStatistics.getMissCount();

				final double listRatio = (double) listHit
						/ (double) (listHit + listMiss);

				System.out.println("\nquery.list-Cache:");
				System.out.println("Hit-Ratio : " + listRatio);
				System.out.println("Hits : " + listHit);
				System.out.println("Miss : " + listMiss);
				System.out.println("\n**************************************");

			} catch (final NullPointerException e) {
				System.out.println("didnt use query.list-Cache");
			}
		} catch (final NullPointerException e) {
			System.out.println("couldnt found statistic");
		}
	}

	/**
	 * Print SearchStatistic.
	 * 
	 * @param statistics
	 */
	public static void getSearchStatistic(final Statistics statistics) {

		try {
			if (!statistics.isStatisticsEnabled()) {
				System.out.println("WARN: statistics disabled");
			}
			System.out.println("\n******** Search-Statistic ********");

			SecondLevelCacheStatistics metaDataStatistics = null;
			try {
				metaDataStatistics = statistics
						.getSecondLevelCacheStatistics("search.metadata");

				final long metaDataHit = metaDataStatistics.getHitCount();

				final long metaDataMiss = metaDataStatistics.getMissCount();

				final double metaDataRatio = (double) metaDataHit
						/ (double) (metaDataHit + metaDataMiss);

				System.out.println("\nsearch.metadata-Cache:");
				System.out.println("Hit-Ratio : " + metaDataRatio);
				System.out.println("Hits : " + metaDataHit);
				System.out.println("Miss : " + metaDataMiss);
			} catch (final NullPointerException e) {
				System.out.println("didnt use search.metadata-Cache");
			}

			SecondLevelCacheStatistics versionStatistics = null;

			try {
				versionStatistics = statistics
						.getSecondLevelCacheStatistics("search.version");

				final long versionHit = versionStatistics.getHitCount();

				final long versionMiss = versionStatistics.getMissCount();

				final double versionRatio = (double) versionHit
						/ (double) (versionHit + versionMiss);

				System.out.println("\nsearch.version-Cache:");
				System.out.println("Hit-Ratio : " + versionRatio);
				System.out.println("Hits : " + versionHit);
				System.out.println("Miss : " + versionMiss);
			} catch (final NullPointerException e) {
				System.out.println("didnt use search.version-Cache");
			}

			SecondLevelCacheStatistics entityStatistics = null;

			try {
				entityStatistics = statistics
						.getSecondLevelCacheStatistics("search.entity");
				final long entityHit = entityStatistics.getHitCount();

				final long entityMiss = entityStatistics.getMissCount();

				final double entityRatio = (double) entityHit
						/ (double) (entityHit + entityMiss);

				System.out.println("\nsearch.entity-Cache:");
				System.out.println("Hit-Ratio : " + entityRatio);
				System.out.println("Hits : " + entityHit);
				System.out.println("Miss : " + entityMiss);
			} catch (final NullPointerException e) {
				System.out.println("didnt use search.entity-Cache");
			}

			System.out.println("\n**********************************\n");
		} catch (final NullPointerException e) {
			System.out.println("couldnt found statistic");
		}
	}
}
